#include "SubsystemCreatureSpawn.h"
#include "INoiseListener.hpp"
#include "Components/ComponentBody.h"
#include "Components/ComponentPlayer.h"
#include "Player/PlayerData.h"
#include "ComponentNoiseListener.h"


#include "Blocks/LeavesBlock.h"
#include "Blocks/WaterBlock.h"


#include "Subsystems/SubsystemTerrain.h"
#include "Subsystems/SubsystemBodies.h"
#include "Subsystems/SubsystemWeather.h"
#include "Subsystems/SubsystemTimeOfDay.h"
#include "Subsystems/SubsystemGameInfo.h"
#include "Subsystems/SubsystemAnimalEntities.h"
#include "Subsystems/SubsystemPlayers.h"


#include "GameRegistry.hpp"
#include "GameSingleton.h"
#include "GameRandom.hpp"
#include "Log.h"

#include "Managers/BlocksManager.h"
#include "Terrain/ITerrainContentsGenerator.h"
#include "Terrain/TerrainChunk.h"


namespace PocketSurvival
{
    void SubsystemCreatureSpawn::initializeCreatureTypes()
    {
        m_creatureTypeVec.push_back(new CreatureType("Duck", SpawnLocationType::Surface, true, false, 
            [this](CreatureType* typeObj, const Point3& point) -> float
            {
                float num = m_terrain->terrainContentsGenerator->calculateOceanShoreDistance(point.posX, point.posZ);
                int32_t humidity = m_terrain->getHumidity(point.posX, point.posZ);
                int32_t temperature = m_terrain->getTemperature(point.posX, point.posZ);
                int32_t blockContents = Terrain::ExtractContents(m_terrain->getCellValue(point.posX, point.posY - 1, point.posZ));
                int32_t topHeight = m_terrain->getTopHeight(point.posX, point.posZ);
                return (humidity > 8 && temperature > 4 && num > 30.0f && point.posY >= topHeight && (GameSingleton::blocksManager->blocks[blockContents]->isBlock<LeavesBlock*>() || blockContents == 18 || blockContents == 8 || blockContents == 2)) ? 2.5f : 0.0f;
            },
            [this](CreatureType* typeObj, const Point3& point) -> int32_t
            {
                return spawnCreatures(typeObj, "Duck", point, 1);
            })
        );
        m_creatureTypeVec.push_back(new CreatureType("Raven", SpawnLocationType::Surface, true, false, 
            [this](CreatureType* typeObj, const Point3& point) -> float
            {
                float num = m_terrain->terrainContentsGenerator->calculateOceanShoreDistance(point.posX, point.posZ);
                int32_t humidity = m_terrain->getHumidity(point.posX, point.posZ);
                int32_t temperature = m_terrain->getTemperature(point.posX, point.posZ);
                int32_t blockContents = Terrain::ExtractContents(m_terrain->getCellValue(point.posX, point.posY - 1, point.posZ));
                int32_t topHeight = m_terrain->getTopHeight(point.posX, point.posZ);
                return ((humidity <= 8 || temperature <= 4) && num >= 30.0f && point.posY >= topHeight && (GameSingleton::blocksManager->blocks[blockContents]->isBlock<LeavesBlock*>() || blockContents == 62 || blockContents == 8 || blockContents == 2 || blockContents == 7)) ? 2.5f : 0.0f;
            },
            [this](CreatureType* typeObj, const Point3& point) -> int32_t
            {
                return spawnCreatures(typeObj, "Raven", point, 1);
            })
        );
        m_creatureTypeVec.push_back(new CreatureType("Seagull", SpawnLocationType::Surface, true, false, 
            [this](CreatureType* typeObj, const Point3& point) -> float
            {
                float num = m_terrain->terrainContentsGenerator->calculateOceanShoreDistance(point.posX, point.posZ);
                int32_t blockContents = Terrain::ExtractContents(m_terrain->getCellValue(point.posX, point.posY - 1, point.posZ));
                int32_t topHeight = m_terrain->getTopHeight(point.posX, point.posZ);
                return (num > -100.0f && num < 40.0f && point.posY >= topHeight && (blockContents == 18 || blockContents == 7 || blockContents == 6 || blockContents == 62)) ? 2.5f : 0.0f;
            },
            [this](CreatureType* typeObj, const Point3& point) -> int32_t
            {
                return spawnCreatures(typeObj, "Seagull", point, 1);
            })
        );
        m_creatureTypeVec.push_back(new CreatureType("Pigeon", SpawnLocationType::Surface, true, false, 
            [this](CreatureType* typeObj, const Point3& point) -> float
            {
                float num = m_terrain->terrainContentsGenerator->calculateOceanShoreDistance(point.posX, point.posZ);
                int32_t temperature = m_terrain->getTemperature(point.posX, point.posZ);
                int32_t blockContents = Terrain::ExtractContents(m_terrain->getCellValue(point.posX, point.posY - 1, point.posZ));
                int32_t topHeight = m_terrain->getTopHeight(point.posX, point.posZ);
                return (temperature > 3 && num >= 30.0f && point.posY >= topHeight && (GameSingleton::blocksManager->blocks[blockContents]->isBlock<LeavesBlock*>() || blockContents == 8 || blockContents == 2 || blockContents == 7)) ? 1.5f : 0.0f;
            },
            [this](CreatureType* typeObj, const Point3& point) -> int32_t
            {
                return spawnCreatures(typeObj, "Pigeon", point, 1);
            })
        );
        m_creatureTypeVec.push_back(new CreatureType("Pigeon", SpawnLocationType::Surface, true, false, 
            [this](CreatureType* typeObj, const Point3& point) -> float
            {
                float num = m_terrain->terrainContentsGenerator->calculateOceanShoreDistance(point.posX, point.posZ);
                int32_t temperature = m_terrain->getTemperature(point.posX, point.posZ);
                int32_t blockContents = Terrain::ExtractContents(m_terrain->getCellValue(point.posX, point.posY - 1, point.posZ));
                int32_t topHeight = m_terrain->getTopHeight(point.posX, point.posZ);
                return (temperature > 3 && num >= 20.0f && point.posY >= topHeight && (GameSingleton::blocksManager->blocks[blockContents]->isBlock<LeavesBlock*>() || blockContents == 8 || blockContents == 2 || blockContents == 7)) ? 2.0f : 0.0f;
            },
            [this](CreatureType* typeObj, const Point3& point) -> int32_t
            {
                int32_t count3 = GameSingleton::gameRandom->getInt(1, 2);
                return spawnCreatures(typeObj, "Pigeon", point, count3);
            })
        );

    }



    int32_t SubsystemCreatureSpawn::spawnCreatures(CreatureType *creatureType, const std::string& templateName, const Point3& point, int32_t count)
    {
        // Log::Info("spawnCreatures");

        int32_t addCount = 0;
        int32_t num = 0;
        while(count > 0 && num < 50)
        {
            Point3 spawnPoint = point;
            if (num > 0)
            {
                spawnPoint.posX += GameSingleton::gameRandom->getInt(-8, 8);
                spawnPoint.posY += GameSingleton::gameRandom->getInt(-4, 8);
                spawnPoint.posZ += GameSingleton::gameRandom->getInt(-8, 8);
            }
            std::shared_ptr<Point3> point2 = processSpawnPoint(spawnPoint, creatureType->spawnLocationType);
            if (point2 != nullptr && creatureType->spawnSuitabilityFunction(creatureType, *point2) > 0.0f)
            {
                Vector3 position(point2->posX + GameSingleton::gameRandom->getFloat(0.4f, 0.6f), point2->posY + 1.1f, point2->posZ + GameSingleton::gameRandom->getFloat(0.4f, 0.6f));
                m_animalEntities->addAnimalEntity(templateName, position);
                count--;
                addCount++;
            }
            num++;
        }
        return addCount;
    }

    // ok
    std::shared_ptr<Point3> SubsystemCreatureSpawn::processSpawnPoint(const Point3& spawnPoint, SpawnLocationType spawnLocationType)
    {
        int32_t x = spawnPoint.posX;
        int32_t num = MathUtils::Clamp(spawnPoint.posY, 1, 254);
        int32_t z = spawnPoint.posZ;
        TerrainChunk *chunkAtCell = m_terrain->getChunkAtCell(x, z);
        if (chunkAtCell != nullptr && chunkAtCell->State >= TerrainChunkState::InvalidContents4)
        {
            for (int32_t i = 0; i < 30; i++)
            {
                Point3 point(x, num + i, z);
                if (testSpawnPoint(point, spawnLocationType))
                {
                    return std::make_shared<Point3>(point);
                }
                Point3 point2(x, num - i, z);
                if (testSpawnPoint(point, spawnLocationType))
                {
                    return std::make_shared<Point3>(point2);
                }
            }
        }
        return nullptr;
    }

    // ok
    bool SubsystemCreatureSpawn::testSpawnPoint(const Point3& spawnPoint, SpawnLocationType spawnLocationType)
    {
        int32_t x = spawnPoint.posX;
        int32_t y = spawnPoint.posY;
        int32_t z = spawnPoint.posZ;
        if (y <= 3 || y >= 253)
        {
            return false;
        }
        switch (spawnLocationType)
        {
        case SpawnLocationType::Surface:
            {
                int32_t cellLightFast = m_terrain->getCellLight(x, y, z);
                if (m_weather->skylightValue - cellLightFast > 3)
                {
                    return false;
                }
                int32_t cellContent = m_terrain->getCellContents(x, y - 1, z);
                int32_t cellContent2 = m_terrain->getCellContents(x, y, z);
                int32_t cellContent3 = m_terrain->getCellContents(x, y + 1, z);
                Block* block6 = GameSingleton::blocksManager->blocks[cellContent];
                Block* block7 = GameSingleton::blocksManager->blocks[cellContent2];
                Block* block8 = GameSingleton::blocksManager->blocks[cellContent3];
                if ((block6->IsCollidable || block6->isBlock<WaterBlock*>()) && 
                    block7->IsCollidable == false && 
                    block7->isBlock<WaterBlock*>() == false && 
                    block8->IsCollidable == false)
                {
                    return (block8->isBlock<WaterBlock*>() == false);
                }
            }
            break;
        case SpawnLocationType::Cave:
            {
                int32_t cellLightFast = m_terrain->getCellLight(x, y, z);
                if (m_weather->skylightValue - cellLightFast < 5)
                {
                    return false;
                }
                int32_t cellContents4 = m_terrain->getCellContents(x, y - 1, z);
                int32_t cellContents5 = m_terrain->getCellContents(x, y, z);
                int32_t cellContents6 = m_terrain->getCellContents(x, y + 1, z);
                Block* block3 = GameSingleton::blocksManager->blocks[cellContents4];
                Block* block4 = GameSingleton::blocksManager->blocks[cellContents5];
                Block* block5 = GameSingleton::blocksManager->blocks[cellContents6];
                if ((block3->IsCollidable || block3->isBlock<WaterBlock*>()) &&
                    block4->IsCollidable == false &&
                    block4->isBlock<WaterBlock*>() == false &&
                    block5->IsCollidable == false)
                {
                    return (block5->isBlock<WaterBlock*>() == false);
                }
            }
            break;
        case SpawnLocationType::Water:
            {
                int32_t cellContents = m_terrain->getCellContents(x, y, z);
                int32_t cellContents2 = m_terrain->getCellContents(x, y + 1, z);
                int32_t cellContents3 = m_terrain->getCellContents(x, y + 2, z);
                Block* obj = GameSingleton::blocksManager->blocks[cellContents];
                Block* block = GameSingleton::blocksManager->blocks[cellContents2];
                Block* block2 = GameSingleton::blocksManager->blocks[cellContents3];
                if (obj->isBlock<WaterBlock*>() && block->IsCollidable == false)
                {
                    return (block2->IsCollidable == false);
                }
            }
            break;
        default:
            break;
        }
        return false;
    }

    int32_t SubsystemCreatureSpawn::countCreature(bool constantSpawn)
    {
        int32_t num = 0;
        auto view = GameSingleton::gameRegistry->view<ComponentBody*>();
        for (entt::entity entity : view)
        {
            ComponentBody* body = view.get<ComponentBody*>(entity);
            if (body->componentCreature != nullptr)
            {
                num++;
            }
        }
        return num;
    }

    int32_t SubsystemCreatureSpawn::countCreatureInArea(const Vector2& c1, const Vector2& c2, bool constantSpawn)
    {
        int32_t num = 0;
        m_componentBodies.clear();
        m_bodies->findBodiesInArea(c1, c2, m_componentBodies);
        for (ComponentBody *body : m_componentBodies)
        {
            if (body->componentCreature != nullptr)
            {
                num++;
            }
        }
        return num;
    }

    void SubsystemCreatureSpawn::spawnCreaturesOnChunk(const Point2& coords, int32_t maxAttempts, bool constantSpawn)
    {
        int32_t num = constantSpawn ? m_totalLimitConstant : m_totalLimit;
        int32_t num2 = constantSpawn ? m_areaLimitConstant : m_areaLimit;
        float v = constantSpawn ? m_areaRadiusConstant : m_areaRadius;

        m_players->getAllPlayerData(m_playerVec);
        num *= m_playerVec.size();
        m_playerVec.clear();

        int32_t num3 = countCreature(constantSpawn);
        Vector2 c2(coords.posX * 16 - v, coords.posY * 16 - v);
        Vector2 c3((coords.posX + 1) * 16 + v, (coords.posY + 1) * 16 + v);
        int32_t num4 = countCreatureInArea(c2, c3, constantSpawn);
        for (int32_t i = 0; i < maxAttempts; i++)
        {
            if (num3 >= num)
            {
                break;
            }
            if (num4 >= num2)
            {
                break;
            }
            SpawnLocationType type = (SpawnLocationType)GameSingleton::gameRandom->getInt(0, 2);
            std::shared_ptr<Point3> spawnPoint = getRandomChunkSpawnPoint(coords, type);
            if (spawnPoint != nullptr)
            {
                std::vector<CreatureType*> source;
                std::vector<float> items;
                for(CreatureType * c : m_creatureTypeVec)
                {
                    if (c->spawnLocationType == type && c->randomSpawn)
                    {
                        source.push_back(c);
                        items.push_back(c->spawnSuitabilityFunction(c, *spawnPoint));
                    }
                }
                int32_t randomWeightedItem = getRandomWeightedItem(items);
                if (randomWeightedItem >= 0)
                {
                    CreatureType *creatureType = source[randomWeightedItem];
                    int32_t num5 = creatureType->spawnFunction(creatureType, *spawnPoint);
                    num3 += num5;
                    num4 += num5;
                }
            }
        }
    }

    void SubsystemCreatureSpawn::spawnRandomCreature()
    {
        if (countCreature(false) < m_totalLimit)
        {

            m_players->getAllPlayerData(m_playerVec);
            for (PlayerDataPtr playerDataPtr : m_playerVec)
            {
                Vector3 position;
                playerDataPtr->componentPlayer->componentBody->getPosition(position);

                Vector2 v(position.posX, position.posZ);
                if (countCreatureInArea(v - Vector2(60.0f), v + Vector2(60.0f), false) >= m_maxPlayerAreaLimit)
                {
                    continue;
                }
                SpawnLocationType type = (SpawnLocationType)GameSingleton::gameRandom->getInt(0, 2);
                std::shared_ptr<Point3> spawnPoint = getRandomSpawn(position, type);
                if (spawnPoint != nullptr)
                {
                    Vector2 c2(spawnPoint->posX - 16.0f, spawnPoint->posZ - 16.0f);
                    Vector2 c3(spawnPoint->posX + 16.0f, spawnPoint->posZ + 16.0f);
                    if (countCreatureInArea(c2, c3, false) >= m_maxPointLimit)
                    {
                        continue;
                    }

                    std::vector<CreatureType*> source;
                    std::vector<float> items;
                    for(CreatureType * c : m_creatureTypeVec)
                    {
                        if (c->spawnLocationType == type && c->randomSpawn)
                        {
                            source.push_back(c);
                            items.push_back(c->spawnSuitabilityFunction(c, *spawnPoint));
                        }
                    }
                    int32_t randomWeightedItem = getRandomWeightedItem(items);
                    if (randomWeightedItem >= 0)
                    {
                        CreatureType *creatureType = source[randomWeightedItem];
                        creatureType->spawnFunction(creatureType, *spawnPoint);
                    }
                }
            }
            m_playerVec.clear();
        }
    }

    std::shared_ptr<Point3> SubsystemCreatureSpawn::getRandomChunkSpawnPoint(const Point2& coords, SpawnLocationType spawnLocationType)
    {
        for (int32_t i = 0; i < 5; i++)
        {
            int32_t x = 16 * coords.posX + GameSingleton::gameRandom->getInt(0, 15);
            int32_t y = GameSingleton::gameRandom->getInt(10, 246);
            int32_t z = 16 * coords.posY + GameSingleton::gameRandom->getInt(0, 15);
            std::shared_ptr<Point3> result = processSpawnPoint(Point3(x, y ,z), spawnLocationType);
            if (result != nullptr)
            {
                return result;
            }
        }
        return nullptr;
    }

    std::shared_ptr<Point3> SubsystemCreatureSpawn::getRandomSpawn(const Vector3& position, SpawnLocationType spawnLocationType)
    {
        for (int32_t i = 0; i < 8; i++)
        {
            int32_t x = Terrain::ToCell(position.posX) + GameSingleton::gameRandom->getSign() * GameSingleton::gameRandom->getInt(20, 40);
            int32_t y = MathUtils::Clamp(Terrain::ToCell(position.posY) + GameSingleton::gameRandom->getInt(-30, 30), 2, 254);
            int32_t z = Terrain::ToCell(position.posZ) + GameSingleton::gameRandom->getSign() * GameSingleton::gameRandom->getInt(20, 40);
            std::shared_ptr<Point3> result = processSpawnPoint(Point3(x, y, z), spawnLocationType);
            if (result != nullptr)
            {
                return result;
            }
        }
        return nullptr;
    }

    int32_t SubsystemCreatureSpawn::getRandomWeightedItem(const std::vector<float> &items)
    {
        float max = 0.0f;
        for (float value : items)
        {
            max += value;
        }
        float num = GameSingleton::gameRandom->getFloat(0.0f, max);
        int32_t num2 = 0;
        for (float item : items)
        {
            if (num < item)
            {
                return num2;
            }
            num -= item;
            num2++;
        }
        return -1;
    }




    void SubsystemCreatureSpawn::onTerrainChunkAdded(TerrainChunk *terrainChunk)
    {
        m_loadChunkQueue.enqueue(terrainChunk->coords);
    }

    void SubsystemCreatureSpawn::update(float dt)
    {
        if (m_gameInfo->environmentBehaviorMode == EnvironmentBehaviorMode::Living)
        {
            Point2 point;
            while (m_loadChunkQueue.try_dequeue(point))
            {
                if (m_loadedChunk.find(point) == m_loadedChunk.end())
                {
                    m_loadedChunk.insert(point);
                    spawnCreaturesOnChunk(point, 5, false);
                }
            }
            if (m_timeOfDay->periodicGameTimeEvent(60.0, 2.0))
            {
                spawnRandomCreature();
            }
        }
    }

    void SubsystemCreatureSpawn::load(const nlohmann::json &json)
    {
        m_gameInfo = GameSingleton::singletonPtr->getSingleton<SubsystemGameInfo>();
        m_terrain = GameSingleton::singletonPtr->getSingleton<SubsystemTerrain>();
        m_timeOfDay = GameSingleton::singletonPtr->getSingleton<SubsystemTimeOfDay>();
        m_weather = GameSingleton::singletonPtr->getSingleton<SubsystemWeather>();
        m_bodies = GameSingleton::singletonPtr->getSingleton<SubsystemBodies>();
        m_animalEntities = GameSingleton::singletonPtr->getSingleton<SubsystemAnimalEntities>();
        m_players = GameSingleton::singletonPtr->getSingleton<SubsystemPlayers>();

        initializeCreatureTypes();
    }

    static const std::string subsystemName = "CreatureSpawn";
    const std::string &SubsystemCreatureSpawn::getName() const
    {
        return subsystemName;
    }
    SubUpdateType SubsystemCreatureSpawn::getUpdateType()
    {
        return SubUpdateType::MultiThreadUpdate;
    }

} // namespace PocketSurvival
